package 更新类

import (
	"context"
	"errors"
	"fmt"
	下载类 "gitee.com/go_888/extends/downloader"
	数组类 "gitee.com/go_888/extends/goframe/garray"
	压缩类 "gitee.com/go_888/extends/goframe/gcompress"
	文件类 "gitee.com/go_888/extends/goframe/gfile"
	map类 "gitee.com/go_888/extends/goframe/gmap"
	加密md5类 "gitee.com/go_888/extends/goframe/gmd5"
	内存锁类 "gitee.com/go_888/extends/goframe/gmlock"
	文本类 "gitee.com/go_888/extends/goframe/gstr"
	安全变量类 "gitee.com/go_888/extends/goframe/gtype"
	配置类 "gitee.com/go_888/extends/ini"
	协程池类 "gitee.com/go_888/goframe/os/grpool"
	时间类 "gitee.com/go_888/goframe/os/gtime"
	"github.com/gogf/gf/v2/os/gctx"
	"github.com/qiniu/go-sdk/v7/cdn"
	"path/filepath"
	"strconv"
	"sync"
)

var 防盗链key string
var 防盗链秒 int

type X版本信息 struct {
	X版本号     string
	X更新说明    string
	X更新日期    string
	X完整链接    string
	X更新列表map map类.X结构_StrStrMap
	X文件列表s   数组类.X结构_文本数组
}

type X版本 struct {
	X版本号  []string
	ini配置 *配置类.X配置项
	根链接   string
}

// X设置全局防盗链 用于设置全局的防盗链参数。
// key: 指定防盗链的密钥，用于生成和验证防盗链URL。
// 秒: 防盗链的有效时间（单位为秒），超过该时间防盗链URL失效。
// 返回: 无返回值，函数直接更新全局变量 `防盗链key` 和 `防盗链秒`。
func X设置全局防盗链(key string, 秒 int) {
	防盗链key = key
	防盗链秒 = 秒
}

// X生成防盗链url 生成带有防盗链的URL。
// 链接: 原始的URL链接。
// 增加随机参数: 是否需要在URL中添加随机参数。
// 返回: 返回带有防盗链的URL字符串。
func X生成防盗链url(链接 string, 增加随机参数 bool) string {
	// 如果需要增加随机参数，则在URL中添加随机生成的参数，以防止CDN缓存。
	if 增加随机参数 {
		链接 = 链接 + "?spm=" + strconv.FormatInt(时间类.X取时间戳微秒(), 10)
	}
	// 如果防盗链的key不为空且秒数大于0，则生成带有时间戳的防盗链URL。
	if 防盗链key != "" && 防盗链秒 > 0 {
		链接, _ = cdn.CreateTimestampAntileechURL(链接, 防盗链key, int64(防盗链秒))
		//工具类.X调试输出("防盗链链接:" + 链接)
	}
	// 返回最终生成的带有防盗链的URL。
	return 链接
}

// X创建 初始化并验证配置链接，解析其内容为版本配置对象。
// 参数:
// ini链接: 指向配置文件（INI 格式）的 URL 地址。
// 返回:
// *X版本: 成功时返回一个包含版本信息和配置的结构体指针。
// error: 如果失败则返回错误信息。
/*
ini文件格式如下:
[1.3.6-beta]
[1.3.5]
[1.3.1]
[1.0.13]
[1.0.12]
[1.0.11]
[1.0.10]
*/
func X创建(ini链接 string) (*X版本, error) {
	状态码, 响应体 := 发送get(X生成防盗链url(ini链接, true))

	if 状态码 != 200 || 响应体 == "" {
		return nil, errors.New("配置链接出错,请确定是否为空或者链接出错")
	}
	ini配置, err := 配置类.X创建并从文本(响应体, true)
	if err != nil {
		return nil, errors.New("ini配置类解析出错,请确定格式是否有误:" + 响应体)
	}
	// 加载配置，并获取所有的版本。
	所有节名 := 数组类.X创建文本并从数组(ini配置.X取所有节名())
	所有节名.X遍历删除(func(索引 int, 值 string) bool {
		if 文本类.X到小写(值) == "default" {
			return true
		}
		return false
	})

	if 所有节名.X取成员数() == 0 {
		return nil, errors.New("ini版本配置为空内容." + 响应体)
	}
	// 处理返回结构。
	return &X版本{
		X版本号排序(所有节名.X取数组()),
		ini配置,
		文本类.X取左边(ini链接, 文本类.X取右边并倒找与含关键字(ini链接, "/")),
	}, err
}

// X取配置值 根据名称获取配置的值。
// 如果 更新 或 更新的 ini配置 成员为 nil，则返回空字符串。
// 参数:
// 名称: 配置项的名称。
// 返回值:
// 配置项的值作为字符串返回，如果无法获取到值或进行了空值处理，则返回空字符串。
func (更新 *X版本) X取配置值(名称 string) string {
	if 更新 != nil && 更新.ini配置 != nil {
		return 文本类.X删首尾空与字符(更新.ini配置.X取文本值("", 名称))
	}
	return ""
}

// X是否有更新 检查当前版本是否为最新版本。
// 参数:
// 当前版本: 当前运行的版本号字符串。
// 忽略测试版: 是否忽略带有"-"符号的测试版本。
// 返回值:
// bool: 如果存在比当前版本更高的版本且符合更新条件，则返回true；否则返回false。
func (更新 *X版本) X是否有更新(当前版本 string, 忽略测试版 bool) bool {
	if 更新 == nil || 当前版本 == "" {
		return false
	}
	版本号数组 := 更新.X版本号
	if 忽略测试版 {
		版本号数组 = 数组类.X创建文本并从数组复制(更新.X版本号).X遍历删除(func(索引 int, 值 string) bool {
			return 文本类.X是否包含(值, "-")
		}).X取数组()
	}
	if len(版本号数组) == 0 {
		return false
	}

	// 比较当前版本和最新版本号，如果当前版本不是最新的，则返回true。
	if X取最新版本号([]string{当前版本, 版本号数组[0]}) != 当前版本 {
		return true
	}
	// 如果当前版本是最新的，则返回false。
	return false
}

// X取新版信息 获取最新的版本信息。
// 参数:
// 忽略测试版: 是否忽略带有"-"符号的测试版本。
// 返回值:
// X版本信息: 最新版本的详细信息。
// error: 如果获取失败，返回相应的错误。
func (更新 *X版本) X取新版信息(忽略测试版 bool) (X版本信息, error) {
	// 检查更新对象是否为空，如果为空则返回错误。
	if 更新 == nil {
		return X版本信息{}, errors.New("错误: 更新对象为空.")
	}

	版本号数组 := 更新.X版本号
	if 忽略测试版 {
		版本号数组 = 数组类.X创建文本并从数组复制(更新.X版本号).X遍历删除(func(索引 int, 值 string) bool {
			return 文本类.X是否包含(值, "-")
		}).X取数组()
	}
	最新版本号 := X取最新版本号(版本号数组)
	return 更新.X取版本信息(最新版本号)
}

// X取版本信息 根据版本号获取版本信息。
// 函数会根据给定的版本号，从配置文件中获取相应的版本信息。
// 参数:
// 版本号: 指定的版本号。
// 返回值:
// X版本信息: 包含版本信息的结构体。
// error: 如果获取失败，返回相应的错误。
func (更新 *X版本) X取版本信息(版本号 string) (X版本信息, error) {
	// 检查更新对象是否为空，如果为空则返回错误。
	if 更新 == nil {
		return X版本信息{}, errors.New("错误: 更新对象为空.")
	}

	返回 := X版本信息{}
	b := 文本类.X删所有空与字符(版本号)
	更新说明链接 := 更新.根链接 + "/" + b + "/_更新说明.txt"
	更新日期链接 := 更新.根链接 + "/" + b + "/_更新日期.txt"
	完整安装包链接 := 更新.根链接 + "/" + b + "/_完整安装包.zip"
	更新列表链接 := 更新.根链接 + "/" + b + "/_更新列表.txt"
	文件列表链接 := 更新.根链接 + "/" + b + "/_文件列表.txt"

	if b2, _ := X判断链接是否为下载链接(X生成防盗链url(更新说明链接, true)); b2 == false {
		return 返回, errors.New("错误: 链接有误,请确认文件是否存在 " + X生成防盗链url(更新说明链接, false))
	}
	if b2, _ := X判断链接是否为下载链接(X生成防盗链url(更新日期链接, false)); b2 == false {
		return 返回, errors.New("错误: 链接有误,请确认文件是否存在 " + X生成防盗链url(更新日期链接, false))
	}
	if b2, _ := X判断链接是否为下载链接(X生成防盗链url(完整安装包链接, false)); b2 == false {
		return 返回, errors.New("错误: 链接有误,请确认文件是否存在 " + X生成防盗链url(完整安装包链接, false))
	}
	if b2, _ := X判断链接是否为下载链接(X生成防盗链url(更新列表链接, false)); b2 == false {
		return 返回, errors.New("错误: 链接有误,请确认文件是否存在 " + X生成防盗链url(更新列表链接, false))
	}
	if b2, _ := X判断链接是否为下载链接(X生成防盗链url(文件列表链接, false)); b2 == false {
		return 返回, errors.New("错误: 链接有误,请确认文件是否存在 " + X生成防盗链url(文件列表链接, false))
	}

	返回.X完整链接 = 完整安装包链接
	返回.X版本号 = 版本号
	_, 返回.X更新说明 = 发送get(X生成防盗链url(更新说明链接, true))
	_, 返回.X更新日期 = 发送get(X生成防盗链url(更新日期链接, false))
	状态码, 更新列表 := 发送get(X生成防盗链url(更新列表链接, false))
	if 状态码 != 200 || 更新列表 == "" {
		return 返回, errors.New("错误:获取更新列表出错: " + X生成防盗链url(更新列表链接, false))
	}
	状态码, 文件列表 := 发送get(X生成防盗链url(文件列表链接, false))
	if 状态码 != 200 || 更新列表 == "" {
		return 返回, errors.New("错误:获取文件列表出错: " + X生成防盗链url(文件列表链接, false))
	}

	for _, s := range 文本类.X分割(更新列表, "\n") {
		更新路径s := 文本类.X分割(s, "||")
		if len(更新路径s) == 2 {
			相对路径 := 文本类.X替换(文本类.X到小写(更新路径s[0]), "\\", 文件类.X常量_文件_系统分隔符)
			返回.X更新列表map.X设置值(相对路径, 更新路径s[1])
		}
	}

	返回.X文件列表s.X设置数组(文本类.X分割(文件列表, "\n"))
	return 返回, nil
}

// X取所有版本号 返回当前可用的所有版本号。
// 参数:
// 忽略测试版: 是否忽略带有"-"符号的测试版本。
// 返回值:
// []string: 版本号字符串切片。
func (更新 *X版本) X取所有版本号(忽略测试版 bool) []string {
	版本号数组 := 更新.X版本号
	if 忽略测试版 {
		版本号数组 = 数组类.X创建文本并从数组复制(更新.X版本号).X遍历删除(func(索引 int, 值 string) bool {
			return 文本类.X是否包含(值, "-")
		}).X取数组()

	}
	return 版本号数组
}

// X下载完整新版 下载最新版本的完整安装包。
// 参数:
// 忽略测试版: 是否忽略带有"-"符号的测试版本。
// 回调函数: 下载进度回调函数，用于报告下载进度。
// 返回值:
// string: 下载完成后的文件保存路径。
// error: 如果下载过程中发生错误，返回相应的错误。
func (更新 *X版本) X下载完整新版(忽略测试版 bool, 回调函数 func(进度 下载类.X进度详情) bool) (string, error) {
	// 检查更新对象是否为空，如果为空则返回错误。
	if 更新 == nil {
		return "", errors.New("下载失败,更新对象为空,是否链接有误?")
	}

	// 获取最新版本信息，根据是否忽略测试版的标志。
	最新版, err := 更新.X取新版信息(忽略测试版)
	if err != nil {
		return "", err
	}

	保存文件名, _ := 加密md5类.X加密(最新版.X完整链接)
	下载路径 := 文件类.X取临时目录(保存文件名 + ".zip")
	文件类.X删除(下载路径)
	err = 下载类.X下载文件(X生成防盗链url(最新版.X完整链接, false), 下载路径, 5, 回调函数)

	if err != nil {
		return "", err
	}
	return 下载路径, nil
}

// X更新文件 用于从服务器下载指定的更新文件并解压后写入本地。
// 下载链接基于最新版本信息和防盗链机制生成，并通过回调函数报告下载进度。
//
// 参数:
//   - 本地路径: 文件解压后的目标路径，为空则使用默认路径。
//   - 文件路径: 要更新的文件在服务器上的相对路径（支持斜杠或反斜杠）。
//   - 忽略测试版: 是否忽略带有"-"符号的测试版本。
//   - 回调函数: 下载进度回调函数，类型为 func(下载类.X进度详情) bool，
//     返回 false 可中断下载过程。
//
// 返回值:
// - error: 如果操作成功返回 nil；否则返回具体错误信息。
func (更新 *X版本) X更新文件(本地路径, 文件路径 string, 忽略测试版 bool, 回调函数 func(进度 下载类.X进度详情) bool) error {
	文件路径 = 文本类.X替换(文件路径, "/", "\\")
	if 文本类.X按长度取文本(文件路径, 0, 1) != "\\" {
		文件路径 = "\\" + 文件路径
	}

	// 检查更新对象是否为空，如果为空则返回错误。
	if 更新 == nil {
		return errors.New("下载失败,更新对象为空,是否链接有误?")
	}

	// 获取最新版本信息，根据是否忽略测试版的标志。
	最新版, err := 更新.X取新版信息(忽略测试版)
	if err != nil {
		return err
	}

	文件名 := 最新版.X更新列表map.X取值(文本类.X到小写(文件路径))
	if 文件名 == "" {
		return errors.New("下载失败,文件不存在,是否文件路径有误? " + 文件路径)
	}

	文件链接 := 更新.根链接 + "/" + 最新版.X版本号 + "/" + 文件名
	if b2, _ := X判断链接是否为下载链接(X生成防盗链url(文件链接, false)); b2 == false {
		return errors.New("错误: 链接有误,请确认文件是否存在 " + X生成防盗链url(文件链接, false))
	}

	返回字节集, err := 下载类.X下载文件到字节集(X生成防盗链url(文件链接, false), 5, 回调函数)
	if err != nil {
		return err
	}

	解压字节集, err := 压缩类.Gzip解压字节集(返回字节集)
	if err != nil {
		return errors.New("错误: 解压gzip失败,请确认文件是否存在 " + err.Error())
	}

	if 本地路径 == "" {
		return X更新单文件(解压字节集)
	} else {
		return X更新单文件(解压字节集, 本地路径)
	}
}

func (更新 *X版本) X下载最新版(项目路径 string, 忽略测试版 bool, 下载回调 func(路径 string, 进度 下载类.X进度详情) bool) (文件数量 int, err error) {
	// 检查更新对象是否为空，如果为空则返回错误。
	if 更新 == nil {
		return 0, errors.New("更新失败,更新对象为空,是否链接有误?")
	}
	if 文件类.X是否存在目录(项目路径) == false {
		return 0, errors.New("更新失败,本地目录不存在." + 项目路径)
	}

	//删除旧文件缓存.
	文件类.X删除(filepath.Join(项目路径, "~bak"))
	err = 文件类.X删除(filepath.Join(项目路径, "~update"))
	if err != nil {
		return 0, err
	}

	// 获取最新版本信息，根据是否忽略测试版的标志。
	最新版, err := 更新.X取新版信息(忽略测试版)
	if err != nil {
		return 0, err
	}

	type 待更新文件 struct {
		相对路径 string
		本地路径 string
		是否存在 bool
		下载链接 string
		下载路径 string
		解压路径 string
	}

	待更新文件s := []待更新文件{}
	最新版.X更新列表map.X遍历(func(相对路径 string, 值 string) bool {
		文件md5 := 文本类.X取左边(值, ".gz")
		本地路径 := filepath.Join(项目路径, 相对路径)
		本地文件md5, _ := 加密md5类.X加密(文件类.X读字节集(本地路径))

		文件信息 := 待更新文件{
			相对路径: 相对路径,
			本地路径: 本地路径,
			是否存在: true,
			下载链接: 更新.根链接 + "/" + 最新版.X版本号 + "/" + 值,
			下载路径: filepath.Join(文件类.X取临时目录(), 值),
			解压路径: filepath.Join(项目路径, "~update", 相对路径),
		}

		if 文件类.X是否存在(本地路径) == false {
			文件信息.是否存在 = false
		}

		if 本地文件md5 != 文件md5 {
			待更新文件s = append(待更新文件s, 文件信息)
		}
		return true
	})

	if len(待更新文件s) == 0 {
		return 0, nil
	}

	//下载文件, 多任务下载.
	协程池 := 协程池类.New(10)
	ctx := gctx.New()
	wg := sync.WaitGroup{}
	停止下载 := 安全变量类.X创建布尔(false)
	for _, 更新文件 := range 待更新文件s {
		wg.Add(1)
		临时变量 := 更新文件
		协程池.Add(ctx, func(ctx context.Context) {
			if 停止下载.X取值() == false {
				文件类.X删除(临时变量.下载路径 + ".gz")
				下载错误 := 下载类.X下载文件(X生成防盗链url(临时变量.下载链接, false), 临时变量.下载路径+".gz", 5, func(进度 下载类.X进度详情) bool {
					回调结果 := false
					内存锁类.X写锁定_函数("下载回调", func() {
						回调结果 = 下载回调(临时变量.相对路径, 进度)
					})
					return 回调结果
				})
				if 下载错误 != nil {
					err = 下载错误
					停止下载.X设置值(true)
					fmt.Println("出错了", err)
				} else {
					err = 压缩类.Gzip解压文件(临时变量.下载路径+".gz", 临时变量.解压路径)
					if err != nil {
						停止下载.X设置值(true)
						fmt.Println("出错了", err)
					}
				}
			}
			wg.Done()
		})
	}
	wg.Wait()

	if 停止下载.X取值() {
		return 0, err
	}

	文件类.X删除(filepath.Join(文件类.X取临时目录(), "~download"))
	return len(待更新文件s), nil
}

func (更新 *X版本) X替换最新版(项目路径 string) (err error) {
	if 更新 == nil {
		return errors.New("更新失败,更新对象为空,是否链接有误?")
	}
	if 文件类.X是否存在目录(项目路径) == false {
		return errors.New("更新失败,本地目录不存在." + 项目路径)
	}

	//删除旧文件缓存.
	文件类.X删除(filepath.Join(项目路径, "~bak"))
	待替换文件s, _ := 文件类.X枚举文件与目录(filepath.Join(项目路径, "~update"), "*.*", true)

	//替换更新文件
	for _, 替换文件 := range 待替换文件s {
		替换路径 := 文本类.X替换并忽略大小写(替换文件, "\\~update\\", "\\")
		备份路径 := 文本类.X替换并忽略大小写(替换文件, "\\~update\\", "\\~bak\\")
		文件类.X复制文件(替换文件, 备份路径)
		if 文件类.X是否存在文件(替换路径) == true {
			err = X更新单文件(文件类.X读字节集(替换文件), 替换路径)
		} else {
			err = 文件类.X复制文件(替换文件, 替换路径)
		}

		if err != nil {
			println("更新失败,错误:" + err.Error())
			break
		}
	}

	//回滚更新
	if err != nil {
		待恢复文件s, _ := 文件类.X枚举文件与目录(filepath.Join(项目路径, "~bak"), "*.*", true)
		for _, 恢复文件 := range 待恢复文件s {
			备份路径 := 文本类.X替换并忽略大小写(恢复文件, "\\~bak\\", "\\")
			X更新单文件(文件类.X读字节集(恢复文件), 备份路径)
		}
	}

	if err != nil {
		return errors.New("更新失败,错误:" + err.Error())
	}

	//删除旧文件缓存.
	文件类.X删除(filepath.Join(项目路径, "~bak"))
	err = 文件类.X删除(filepath.Join(项目路径, "~update"))
	return nil
}
